﻿using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using NSubstitute;
using Phoenix.ActionResults;
using Phoenix.Diagnostics;
using Phoenix.Exceptions;
using Phoenix.Filters;
using Phoenix.Filters.ResultFilters;
using Phoenix.Frames;
using Phoenix.Framework;
using Phoenix.Tests.Controllers;
using Xunit;

namespace Phoenix.Tests.Framework
{
    public class ActionInvokerFacts : IDisposable
    {
        readonly IPhoenixHost phoenixHost;
        readonly ControllerActionContext currentContext;
        readonly ActionInvoker actionInvoker;
        readonly TestController controller;
        readonly IControllerFactory controllerFactory;
        readonly Type controllerType;

        public ActionInvokerFacts()
        {
            controllerFactory = Substitute.For<IControllerFactory>();
            controller = Substitute.For<TestController>();
            controllerFactory
                .Create(typeof(TestController), Arg.Any<ControllerActionContext>())
                .Returns(c=>
                {
                    var context = c.Arg<ControllerActionContext>();
                    controller.Initialise(context);

                    return controller;
                });
            controllerType = typeof (TestController);
            phoenixHost = Substitute.For<IPhoenixHost>();
            phoenixHost.AppScheduler.Returns(TaskScheduler.Default);
            phoenixHost.AppDispatcher.Returns(Dispatcher.CurrentDispatcher);
            phoenixHost.ControllerLocator.Locate(Arg.Any<ControllerAction>()).Returns(controllerType);
            currentContext = PhoenixObjectMother.CreateContext(ControllerAction.CreateFrom("Current.Action"));
            actionInvoker = new ActionInvoker(new TraceErrorReporter(), ()=>phoenixHost, controllerFactory);
        }

        [Fact]
        public void throws_when_currentcontext_is_empty_and_no_target_frame_is_specified()
        {
            // arrange
            var emptyContext = new ControllerActionContext(phoenixHost);

            // act/assert
            var controllerAction = ControllerAction.CreateFrom("Controller.Action");
            var invokeActionRequest = new InvokeActionRequest(emptyContext, controllerAction, null, InvokeOptions.None);
            Assert.Throws<InvalidOperationException>(() => actionInvoker
                                                               .InvokeAction(invokeActionRequest)
                                                               .Wait(1000));
        }

        [Fact]
        public void can_invoke_controller_action()
        {
            // arrange
            var actionResult = PhoenixObjectMother.CreateActionResult(null);
            controller.TestAction().Returns(actionResult);

            // act
            var controllerAction = ControllerAction.CreateFrom("TestController.TestAction");
            var invokeActionRequest = new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None);
            actionInvoker
                .InvokeAction(invokeActionRequest)
                .Wait(1000);

            // assert
            controller.Received().TestAction();
        }

        [Fact]
        public void can_invoke_controller_action_with_query_string()
        {
            // arrange
            var actionResult = PhoenixObjectMother.CreateActionResult(null);
            controller.TestActionWithQueryString(Arg.Any<QueryStringArgumentsCollection>()).Returns(actionResult);

            // act
            var controllerAction = ControllerAction.CreateFrom("TestController.TestActionWithQueryString");
            var invokeActionRequest = new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None);
            actionInvoker
                .InvokeAction(invokeActionRequest)
                .Wait();

            // assert
            controller.Received().TestActionWithQueryString(Arg.Any<QueryStringArgumentsCollection>());
        }

        [Fact]
        public void executes_action_result_returned_from_controller()
        {
            // arrange
            var actionResult = PhoenixObjectMother.CreateActionResult(null);
            controller.TestAction().Returns(actionResult);

            // act
            var controllerAction = ControllerAction.CreateFrom("TestController.TestAction");
            actionInvoker
                .InvokeAction(new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None))
                .Wait(1000);

            // assert
            actionResult.ReceivedWithAnyArgs().Execute(currentContext.NavigationFrame);
        }

        [Fact]
        public void during_navigation_frame_should_have_active_foreground_task()
        {
            // arrange
            var targetFrame = Substitute.For<IContentFrame>();
            controller.TestAction().Returns(PhoenixObjectMother.CreateActionResult(currentContext));

            // act
            var controllerAction = ControllerAction.CreateFrom("TestController.TestAction");
            var invokeActionRequest = new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None, targetFrame);
            actionInvoker
                .InvokeAction(invokeActionRequest)
                .Wait();

            // assert
            targetFrame.Received().BeginOperation(invokeActionRequest);
            targetFrame.Received().EndOperation(invokeActionRequest);
        }

        [Fact]
        public void when_actionresult_is_ihaveinvokecompletedsteps_it_is_notified_once_execution_is_finished()
        {
            // arrange
            var actionResult = CreateActionResult(null);
            controller.TestAction().Returns((ActionResult)actionResult);

            // act
            var controllerAction = ControllerAction.CreateFrom("TestController.TestAction");
            actionInvoker.InvokeAction(new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None)).Wait(1000);

            // assert
            actionResult.Received().InvokeComplete();
        }

        [Fact]
        public void when_action_invoked_while_action_is_executing_it_is_queued_and_run_when_first_action_completes()
        {
            // arrange
            var sync = new ManualResetEvent(false);
            Task result = null;
            Task result2 = null;
            try
            {
                var controllerAction = new ControllerAction("TestController", "TestAction", null);
                var controllerAction2 = new ControllerAction("TestController", "TestAction2", null);
                controller.TestAction()
                    .Returns(c =>
                    {
                        sync.WaitOne(5000);
                        return PhoenixObjectMother.CreateActionResult(currentContext);
                    });
                controller.TestAction2().Returns(PhoenixObjectMother.CreateActionResult(currentContext));

                // act
                result = actionInvoker.InvokeAction(new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None));
                result2 = actionInvoker.InvokeAction(new InvokeActionRequest(currentContext, controllerAction2, null, InvokeOptions.None));

                // assert
                Assert.Equal(actionInvoker.RequestQueue[currentContext.ContentFrame].Peek().Key.Target, controllerAction2);
            }
            finally
            {
                sync.Set();
                Task.WaitAll(result, result2);
            }
        }

        [Fact]
        public void when_action_is_queued_its_setup_is_preserved()
        {
            // arrange
            var flag = false;
            var sync = new ManualResetEvent(false);
            Task result = null;
            Task result2 = null;
            try
            {
                var controllerAction = new ControllerAction("TestController", "TestAction", null);
                var controllerAction2 = new ControllerAction("TestController", "TestAction2", null);
                controller.TestAction()
                    .Returns(c =>
                    {
                        sync.WaitOne(5000);
                        return PhoenixObjectMother.CreateActionResult(currentContext);
                    });
                controller.TestAction2().Returns(PhoenixObjectMother.CreateActionResult(currentContext));

                // act
                result = actionInvoker.InvokeAction(new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None));
                Action<ControllerActionContext> contextSetup = c => flag = true;
                result2 = actionInvoker.InvokeInternal(new InvokeActionRequest(currentContext, controllerAction2, null, InvokeOptions.None),
                    contextSetup);

                // assert
                Assert.Equal(actionInvoker.RequestQueue[currentContext.ContentFrame].Peek().Value, contextSetup);
            }
            finally
            {
                sync.Set();
                Task.WaitAll(result, result2);
                //Verify additional context setup is complete
                Assert.True(flag);
            }
        }

        [Fact(Skip = "Busted")]
        public void action_invoker_allows_concurrent_invokes_for_different_frames()
        {
            // arrange
            //var navigationFrame = Substitute.For<INavigationFrame>();
            //var navigationFrame2 = Substitute.For<INavigationFrame>();
            //var controllerAction = new ControllerAction("TestController", "TestAction", null);
            //var controllerAction2 = new ControllerAction("TestController", "TestAction", null);
            //controller.TestAction().Returns(c=> PhoenixObjectMother.CreateActionResult(currentContext));

            //// act
            //var result = actionInvoker.InvokeAction(new InvokeActionRequest(currentContext, controllerAction, null, navigationFrame));
            //var result2 = actionInvoker.InvokeAction(new InvokeActionRequest(currentContext, controllerAction2, null, navigationFrame2));
            
            //// assert
            //return 
        }

        [Fact]
        public void action_invoker_throws_when_when_resolving_controller_action_fails()
        {
            // arrange
            phoenixHost.ControllerLocator
                .Locate(Arg.Any<ControllerAction>())
                .Returns(c => { throw new ControllerNotFoundException("Something"); });

            // act/assert
            var invokeActionRequest = new InvokeActionRequest(currentContext, ControllerAction.CreateFrom("TestController.TestAction"), null, InvokeOptions.None);
            var result = actionInvoker.InvokeAction(invokeActionRequest);

            Exception expectedException = null;
            try
            {
                result.Wait();
            }
            catch (Exception ex)
            {
                expectedException = ex;
            }

            Assert.IsType<AggregateException>(expectedException);
            var innerException = ((AggregateException)expectedException).Flatten().InnerException;
            Assert.IsType<ControllerNotFoundException>(innerException);
        }

        [Fact]
        public void invoke_task_is_faulted_when_action_result_throws()
        {
            // arrange
            controller
                .TestAction()
                .Returns(new DelegateActionResult(() => { throw new InvalidOperationException(); }));

            // act
            var controllerAction = ControllerAction.CreateFrom("TestController.TestAction");
            var result = actionInvoker.InvokeAction(new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None));

            // assert
            try
            {
                result.Wait();
            }
            catch (Exception ex)
            {
                Assert.IsType<AggregateException>(ex);
                var innerException = ((AggregateException)ex).Flatten().InnerException;
                Assert.IsType<ControllerActionInvocationFailedException>(innerException);
                var originalException = innerException.InnerException;
                Assert.IsType<InvalidOperationException>(originalException);
            }
        }

        [Fact]
        public void action_invoker_targets_current_contexts_navigation_frame_by_default()
        {
            // arrange 
            var controllerAction = ControllerAction.CreateFrom("TestController.TestAction");
            var navigationFrame = Substitute.For<INavigationFrame>();
            var currentFrame = Substitute.For<IContentFrame>();
            var context = PhoenixObjectMother.CreateContext(controllerAction, navigationFrame, currentFrame);
            var actionResult = PhoenixObjectMother.CreateActionResult(null);
            controller.TestAction().Returns(actionResult);

            // act
            actionInvoker
                .InvokeAction(new InvokeActionRequest(context, controllerAction, null, InvokeOptions.None))
                .Wait(1000);

            // assert
            controller.Received().TestAction();
            actionResult.ReceivedWithAnyArgs().Execute(navigationFrame);
        }

        [Fact]
        public void executes_global_result_filters_executingevent_before_executing()
        {
            // arrange
            var controllerAction = ControllerAction.CreateFrom("TestController.TestAction");
            var context = PhoenixObjectMother.CreateContext(controllerAction);
            var actionResult = PhoenixObjectMother.CreateActionResult(null);
            var resultFilter = Substitute.For<IResultFilter>();
            actionResult
                .When(r => r.Execute(Arg.Any<IContentFrame>()))
                .Do(c =>
                    {
                        /*assert*/
                        resultFilter.Received().OnResultExecuting(Arg.Is<ResultExecutingContext>(r => r.PreviousContext == context));
                        resultFilter.DidNotReceiveWithAnyArgs().OnResultExecuted(null);
                    });

            controller.TestAction().Returns(c => actionResult);

            var filterProvider = Substitute.For<IFilterProvider>();
            filterProvider
                .GetFilters(Arg.Any<ControllerContext>(), Arg.Any<IActionDescriptor>())
                .Returns(new[] { new Filter(resultFilter, FilterScope.Global) });

            FilterProviders.Providers.Add(filterProvider);

            // act
            actionInvoker
                .InvokeAction(new InvokeActionRequest(context, controllerAction, null, InvokeOptions.None))
                .Wait();

            // assert
            /*see above in actionResult execute method*/
        }

        [Fact]
        public void executes_global_result_filters_executedevent_after_executing()
        {
            // arrange
            var controllerAction = ControllerAction.CreateFrom("TestController.TestAction");
            var actionResult = PhoenixObjectMother.CreateActionResult(null);
            var resultFilter = Substitute.For<IResultFilter>();
            controller.TestAction().Returns(c => actionResult);
            var filterProvider = Substitute.For<IFilterProvider>();
            filterProvider
                .GetFilters(Arg.Any<ControllerContext>(), Arg.Any<IActionDescriptor>())
                .Returns(new[] { new Filter(resultFilter, FilterScope.Global) });

            FilterProviders.Providers.Add(filterProvider);

            // act
            actionInvoker
                .InvokeAction(new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None))
                .Wait();

            // assert
            resultFilter.Received().OnResultExecuting(Arg.Is<ResultExecutingContext>(r => r.PreviousContext == currentContext));
            resultFilter.Received().OnResultExecuted(Arg.Is<ResultExecutedContext>(r => r.ActionContext.ControllerAction == controllerAction));
        }

        [Fact]
        public void invoketask_returns_value_to_controller()
        {
            // arrange
            controller.TestAction().Returns(c => PhoenixObjectMother.CreateActionResult(currentContext));
            var controllerAction = ControllerAction.CreateFrom("TestController.TestAction");
            controller
                .TestAction()
                .Returns(c=>
                {
                    var controllerActionContext = controller.Context;
                    controllerActionContext.CompletionSource.SetResult(1);
                    return new DoNothingActionResult(controllerActionContext);
                });

            // act
            var invokeActionRequest = new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None);
            var resultTask = actionInvoker.InvokeActionWithResult<int>(invokeActionRequest);
            var result = resultTask.Result;

            // assert
            Assert.Equal(1, result);
        }

        [Fact]
        public void invoketask_returns_cancelled_to_caller()
        {
            // arrange
            controller.TestAction().Returns(c => PhoenixObjectMother.CreateActionResult(currentContext));
            var controllerAction = ControllerAction.CreateFrom("TestController.TestAction");
            controller
                .TestAction()
                .Returns(c=>
                {
                    var controllerActionContext = controller.Context;
                    controllerActionContext.CompletionSource.SetCanceled();
                    return new DoNothingActionResult(controllerActionContext);
                });

            // act
            var invokeActionRequest = new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None);
            var resultTask = actionInvoker.InvokeActionWithResult<int>(invokeActionRequest);

            //assert
            try
            {
                resultTask.Wait();
            }
            catch (Exception ex)
            {
                Assert.IsType<TaskCanceledException>(ex.InnerException);
            }
        }

        [Fact]
        public void invoketask_returns_exception_to_caller()
        {
            // arrange
            var controllerAction = ControllerAction.CreateFrom("Controller.TestAction");
            controller
                .TestAction()
                .Returns(c =>
                {
                    
                    var controllerActionContext = controller.Context;
                    controllerActionContext.CompletionSource.SetException(new InvalidOperationException());
                    return new DoNothingActionResult(controllerActionContext);
                });

            // act
            var invokeActionRequest = new InvokeActionRequest(currentContext, controllerAction, null, InvokeOptions.None);
            var resultTask = actionInvoker.InvokeActionWithResult<int>(invokeActionRequest);

            //assert
            try
            {
                resultTask.Wait();
            }
            catch (Exception ex)
            {
                Assert.IsType<InvalidOperationException>(ex.InnerException);
            }
        }

        static IHaveInvokeCompletedSteps CreateActionResult(ControllerActionContext controllerActionContext)
        {
            return (IHaveInvokeCompletedSteps)Substitute.For(new[]
                                                                 {
                                                                     typeof (ActionResult),
                                                                     typeof (IHaveInvokeCompletedSteps)
                                                                 }, new object[] { controllerActionContext });
        }

        public void Dispose()
        {
            FilterProviders.Providers.Clear();
        }
    }
}